#!@GUILE@
!#
;;; sound.scm --- Display OSD with sound volume

;; Copyright © 2016 Alex Kost <alezost@gmail.com>

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:

;; This is a simple example showing how Guile-XOSD can be used to
;; display sound volume by getting info from 'amixer' output.
;;
;; This script passes all its arguments to 'amixer' and parses its
;; output by searching a line like this:
;;
;;   ... Playback 39118 [60%] [on]
;;
;; or this:
;;
;;   ... Playback 32 [50%] [-32.00dB] [off]
;;
;; Examples of using:
;;
;;   sound.scm sget Master
;;   sound.scm sset Master toggle
;;   sound.scm sset Master 40%
;;
;; See 'man amixer' for more info.  Note: 'amixer' is a program from
;; 'alsa-utils' package (or whatever it is called by your distribution):
;; <http://www.alsa-project.org/>.

;; Actually, this is not a real world example.  It has 2 downsides:
;;
;; 1. At first, well, it's not a compiled C program, it's just a Guile
;; script, so it is not super-fast :-)
;;
;; 2. This script exits only after the sound OSD disappears (the timeout
;; is set to 3 seconds), so, for example, if you bind "sound.scm sset
;; Master 3%+" to some key in your window manager, you'll not be very
;; happy if you quickly press this key several times: the sound will be
;; increased properly (by 3% each time) but you'll see several OSDs
;; laying on each other, as another "sound.scm" script will be called
;; when the previous one(s) is(are) still alive.
;;
;; In practice I use the following solution, that allows me to overcome
;; both downsides: I start Guile-Daemon with the config that sets all
;; this "sound OSD" stuff, and then I just send small Guile expressions
;; to the Daemon for evaluating.  As you can see, I'm still using Guile
;; to set the sound and to display OSD, but it is *very fast* and it's
;; the same OSD object every time!
;;
;; Guile-Daemon: <https://github.com/alezost/guile-daemon>
;; My config for Guile-Daemon: <https://github.com/alezost/guile-daemon-config>

;;; Code:

(use-modules
 (ice-9 format)
 (ice-9 match)
 (ice-9 popen)
 (ice-9 rdelim)
 (ice-9 regex)
 (srfi srfi-11)
 (xosd))

(define %color-muted "#E74F35")
(define %color-unmuted "#23B13E")
(define %color-error "orange")

(define %osd
  (make-osd #:lines 2
            #:timeout 3
            #:position 'bottom
            #:align 'center
            #:font "-*-dejavu sans-bold-r-normal-*-*-300-*-*-p-*-*-1"
            #:shadow-offset 2))

(define* (display-sound osd #:key volume mute?)
  (set-osd-color! osd (if mute? %color-muted %color-unmuted))
  (display-string-in-osd osd (format #f "Volume: ~d%" volume))
  (display-percentage-in-osd osd volume 1))

(define (display-error osd)
  (set-osd-color! osd %color-error)
  (display-string-in-osd osd "")
  (display-string-in-osd osd "Oops, can't parse amixer output :-)" 1))

(define (call-amixer . args)
  "Call 'amixer' with ARGS and return its output."
  (let* ((port (apply open-pipe* OPEN_READ "amixer" args))
         (out (read-string port)))
    (close-pipe port)
    out))

(define (parse-amixer-output output)
  "Parse 'amixer' OUTPUT and return 2 values: sound volume (a number
from 0 to 100) and a boolean value showing if the sound is
muted/unmuted.  Volume is #f if OUTPUT cannot be parsed."
  (let* ((rx (make-regexp
              "Playback [0-9]+ \\[([0-9]+)%\\].* \\[(on|off)\\]"))
         (res (regexp-exec rx output)))
    (if res
        (values (string->number (match:substring res 1))
                (string=? "off" (match:substring res 2)))
        (values #f #f))))

(define (show-help)
  (format #t "Usage: ~a AMIXER-ARGS ...
Run 'amixer AMIXER-ARGS ...', parse its output, and display OSD with the
current sound volume.\n"
          (car (command-line))))

(define (main args)
  (match (cdr args)
    (((or "-h" "--help" "help") _ ...)
     (show-help))
    ((amixer-args ...)
     (let-values (((volume mute?)
                   (parse-amixer-output
                    (apply call-amixer amixer-args))))
       (if volume
           (display-sound %osd #:volume volume #:mute? mute?)
           (display-error %osd))
       (wait-while-osd-displayed %osd)))
    (_ (show-help))))

(when (batch-mode?)
  (main (command-line)))

;;; sound.scm ends here
